/**
 * MojoSetup; a portable, flexible installation application.
 *
 * Please see the file LICENSE.txt in the source's root directory.
 *
 *  This file written by Ryan C. Gordon.
 *
      Copyright (c) 2006-2010 Ryan C. Gordon and others.

   This software is provided 'as-is', without any express or implied warranty.
   In no event will the authors be held liable for any damages arising from
   the use of this software.

   Permission is granted to anyone to use this software for any purpose,
   including commercial applications, and to alter it and redistribute it
   freely, subject to the following restrictions:

   1. The origin of this software must not be misrepresented; you must not
   claim that you wrote the original software. If you use this software in a
   product, an acknowledgment in the product documentation would be
   appreciated but is not required.

   2. Altered source versions must be plainly marked as such, and must not be
   misrepresented as being the original software.

   3. This notice may not be removed or altered from any source distribution.

       Ryan C. Gordon <icculus@icculus.org>
 *
 */

#if !SUPPORT_GUI_NCURSES
#error Something is wrong in the build system.
#endif

#define BUILDING_EXTERNAL_PLUGIN 1
#include "gui.h"

MOJOGUI_PLUGIN(ncurses)

#if !GUI_STATIC_LINK_NCURSES
CREATE_MOJOGUI_ENTRY_POINT(ncurses)
#endif

#include <unistd.h>
#include <ctype.h>
// CMake searches for a whole bunch of different possible curses includes
#if defined(HAVE_NCURSESW_NCURSES_H)
#include <ncursesw/ncurses.h>
#elif defined(HAVE_NCURSESW_CURSES_H)
#include <ncursesw/curses.h>
#elif defined(HAVE_NCURSESW_H)
#include <ncursesw.h>
#else
#error ncurses gui enabled, but no known header file found
#endif

#include <locale.h>

// This was built to look roughly like dialog(1), but it's not nearly as
//  robust. Also, I didn't use any of dialog's code, as it is GPL/LGPL,
//  depending on what version you start with. There _is_ a libdialog, but
//  it's never something installed on any systems, and I can't link it
//  statically due to the license.
//
// ncurses is almost always installed as a shared library, though, so we'll
//  just talk to it directly. Fortunately we don't need much of what dialog(1)
//  offers, so rolling our own isn't too painful (well, compared to massive
//  head trauma, I guess).
//
// Pradeep Padala's ncurses HOWTO was very helpful in teaching me curses
//  quickly: http://tldp.org/HOWTO/NCURSES-Programming-HOWTO/index.html

// !!! FIXME: this should all be UTF-8 and Unicode clean with ncursesw, but
// !!! FIXME:  it relies on the terminal accepting UTF-8 output (we don't
// !!! FIXME:  attempt to convert) and assumes all characters fit in one
// !!! FIXME:  column, which they don't necessarily for some Asian languages,
// !!! FIXME:  etc. I'm not sure how to properly figure out column width, if
// !!! FIXME:  it's possible at all, but for that, you should probably
// !!! FIXME:  go to a proper GUI plugin like GTK+ anyhow.

typedef enum
{
    MOJOCOLOR_BACKGROUND=1,
    MOJOCOLOR_BORDERTOP,
    MOJOCOLOR_BORDERBOTTOM,
    MOJOCOLOR_BORDERTITLE,
    MOJOCOLOR_TEXT,
    MOJOCOLOR_TEXTENTRY,
    MOJOCOLOR_BUTTONHOVER,
    MOJOCOLOR_BUTTONNORMAL,
    MOJOCOLOR_BUTTONBORDER,
    MOJOCOLOR_TODO,
    MOJOCOLOR_DONE,
} MojoColor;


typedef struct
{
    WINDOW *mainwin;
    WINDOW *textwin;
    WINDOW **buttons;
    char *title;
    char *text;
    char **textlines;
    char **buttontext;
    int buttoncount;
    int textlinecount;
    int hoverover;
    int textpos;
    boolean hidecursor;
    boolean ndelay;
    int cursval;
} MojoBox;


static char *lastProgressType = NULL;
static char *lastComponent = NULL;
static boolean lastCanCancel = false;
static uint32 percentTicks = 0;
static char *title = NULL;
static MojoBox *progressBox = NULL;


static void drawButton(MojoBox *mojobox, int button)
{
    const boolean hover = (mojobox->hoverover == button);
    int borderattr = 0;
    WINDOW *win = mojobox->buttons[button];
    const char *str = mojobox->buttontext[button];
    int w, h;
    getmaxyx(win, h, w);

    if (!hover)
        wbkgdset(win, COLOR_PAIR(MOJOCOLOR_BUTTONNORMAL));
    else
    {
        borderattr = COLOR_PAIR(MOJOCOLOR_BUTTONBORDER) | A_BOLD;
        wbkgdset(win, COLOR_PAIR(MOJOCOLOR_BUTTONHOVER));
    } // else

    werase(win);
    wmove(win, 0, 0);
    waddch(win, borderattr | '<');
    wmove(win, 0, w-1);
    waddch(win, borderattr | '>');
    wmove(win, 0, 2);

    if (!hover)
        waddstr(win, str);
    else
    {
        wattron(win, COLOR_PAIR(MOJOCOLOR_BUTTONHOVER) | A_BOLD);
        waddstr(win, str);
        wattroff(win, COLOR_PAIR(MOJOCOLOR_BUTTONHOVER) | A_BOLD);
    } // else
} // drawButton


static void drawText(MojoBox *mojobox)
{
    int i;
    const int tcount = mojobox->textlinecount;
    int pos = mojobox->textpos;
    int w, h;
    WINDOW *win = mojobox->textwin;
    getmaxyx(win, h, w);

    werase(mojobox->textwin);
    for (i = 0; (pos < tcount) && (i < h); i++, pos++)
        mvwaddstr(win, i, 0, mojobox->textlines[pos]);

    if (tcount > h)
    {
        const int pct = (int) ((((double) pos) / ((double) tcount)) * 100.0);
        win = mojobox->mainwin;
        wattron(win, COLOR_PAIR(MOJOCOLOR_BORDERTITLE) | A_BOLD);
        mvwprintw(win, h+1, w-5, "(%3d%%)", pct);
        wattroff(win, COLOR_PAIR(MOJOCOLOR_BORDERTITLE) | A_BOLD);
    } // if
} // drawText


static void drawBackground(WINDOW *win)
{
    wclear(win);
    if (title != NULL)
    {
        int w, h;
        getmaxyx(win, h, w);
        wattron(win, COLOR_PAIR(MOJOCOLOR_BACKGROUND) | A_BOLD);
        mvwaddstr(win, 0, 0, title);
        mvwhline(win, 1, 1, ACS_HLINE, w-2);
        wattroff(win, COLOR_PAIR(MOJOCOLOR_BACKGROUND) | A_BOLD);
    } // if
} // drawBackground


static void confirmTerminalSize(void)
{
    int scrw = 0;
    int scrh = 0;
    char *msg = NULL;
    int len = 0;
    int x = 0;
    int y = 0;

    while (1)   // loop until the window meets a minimum dimension requirement.
    {
        getmaxyx(stdscr, scrh, scrw);
        scrh--; // -1 to save the title at the top of the screen...

        if (scrw < 30)  // too thin
            msg = xstrdup(_("[Make the window wider!]"));
        else if (scrh < 10)  // too short
            msg = xstrdup(_("[Make the window taller!]"));
        else
            break;  // we're good, get out.

        len = utf8len(msg);
        y = scrh / 2;
        x = ((scrw - len) / 2);

        if (y < 0) y = 0;
        if (x < 0) x = 0;

        wclear(stdscr);
        wmove(stdscr, y, x);
        wrefresh(stdscr);
        wmove(stdscr, y, x);
        wattron(stdscr, COLOR_PAIR(MOJOCOLOR_BACKGROUND) | A_BOLD);
        waddstr(stdscr, msg);
        wattroff(stdscr, COLOR_PAIR(MOJOCOLOR_BACKGROUND) | A_BOLD);
        nodelay(stdscr, 0);
        wrefresh(stdscr);
        free(msg);

        while (wgetch(stdscr) != KEY_RESIZE) { /* no-op. */ }
    } // while
} // confirmTerminalSize


static MojoBox *makeBox(const char *title, const char *text,
                        char **buttons, int bcount,
                        boolean ndelay, boolean hidecursor)
{
    MojoBox *retval = NULL;
    WINDOW *win = NULL;
    int scrw, scrh;
    int len = 0;
    int buttonsw = 0;
    int x = 0;
    int y = 0;
    int h = 0;
    int w = 0;
    int texth = 0;
    int i;

    confirmTerminalSize();  // blocks until window is large enough to continue.

    getmaxyx(stdscr, scrh, scrw);
    scrh--; // -1 to save the title at the top of the screen...

    retval = (MojoBox *) xmalloc(sizeof (MojoBox));
    retval->hidecursor = hidecursor;
    retval->ndelay = ndelay;
    retval->cursval = ((hidecursor) ? curs_set(0) : ERR);
    retval->title = xstrdup(title);
    retval->text = xstrdup(text);
    retval->buttoncount = bcount;
    retval->buttons = (WINDOW **) xmalloc(sizeof (WINDOW*) * bcount);
    retval->buttontext = (char **) xmalloc(sizeof (char*) * bcount);

    for (i = 0; i < bcount; i++)
        retval->buttontext[i] = xstrdup(buttons[i]);

    retval->textlines = splitText(retval->text, scrw-4,
                                  &retval->textlinecount, &w);

    len = utf8len(title);
    if (len > scrw-4)
    {
        len = scrw-4;
        strcpy(&retval->title[len-3], "...");  // !!! FIXME: not Unicode safe!
    } // if

    if (len > w)
        w = len;

    if (bcount > 0)
    {
        for (i = 0; i < bcount; i++)
            buttonsw += utf8len(buttons[i]) + 5;  // '<', ' ', ' ', '>', ' '
        if (buttonsw > w)
            w = buttonsw;
        // !!! FIXME: what if these overflow the screen?
    } // if

    w += 4;
    h = retval->textlinecount + 2;
    if (bcount > 0)
        h += 2;

    if (h > scrh-2)
        h = scrh-2;

    x = (scrw - w) / 2;
    y = ((scrh - h) / 2) + 1;

    // can't have negative coordinates, so in case we survived the call to
    //  confirmTerminalSize() but still need more, just draw as much as
    //  possible from the top/left to fill the window.
    if (x < 0) x = 0;
    if (y < 0) y = 0;

    win = retval->mainwin = newwin(h, w, y, x);
	keypad(win, TRUE);
    nodelay(win, ndelay);
    wbkgdset(win, COLOR_PAIR(MOJOCOLOR_TEXT));
    wclear(win);
    waddch(win, ACS_ULCORNER | A_BOLD | COLOR_PAIR(MOJOCOLOR_BORDERTOP));
    whline(win, ACS_HLINE | A_BOLD | COLOR_PAIR(MOJOCOLOR_BORDERTOP), w-2);
    wmove(win, 0, w-1);
    waddch(win, ACS_URCORNER | COLOR_PAIR(MOJOCOLOR_BORDERBOTTOM));
    wmove(win, 1, 0);
    wvline(win, ACS_VLINE | A_BOLD | COLOR_PAIR(MOJOCOLOR_BORDERTOP), h-2);
    wmove(win, 1, w-1);
    wvline(win, ACS_VLINE | COLOR_PAIR(MOJOCOLOR_BORDERBOTTOM), h-2);
    wmove(win, h-1, 0);
    waddch(win, ACS_LLCORNER | A_BOLD | COLOR_PAIR(MOJOCOLOR_BORDERTOP));
    whline(win, ACS_HLINE | COLOR_PAIR(MOJOCOLOR_BORDERBOTTOM), w-2);
    wmove(win, h-1, w-1);
    waddch(win, ACS_LRCORNER | COLOR_PAIR(MOJOCOLOR_BORDERBOTTOM));

    len = utf8len(retval->title);
    wmove(win, 0, ((w-len)/2)-1);
    wattron(win, COLOR_PAIR(MOJOCOLOR_BORDERTITLE) | A_BOLD);
    waddch(win, ' ');
    waddstr(win, retval->title);
    wmove(win, 0, ((w-len)/2)+len);
    waddch(win, ' ');
    wattroff(win, COLOR_PAIR(MOJOCOLOR_BORDERTITLE) | A_BOLD);

    if (bcount > 0)
    {
        const int buttony = (y + h) - 2;
        int buttonx = (x + w) - ((w - buttonsw) / 2);
        wmove(win, h-3, 1);
        whline(win, ACS_HLINE | A_BOLD | COLOR_PAIR(MOJOCOLOR_BORDERTOP), w-2);
        for (i = 0; i < bcount; i++)
        {
            len = utf8len(buttons[i]) + 4;
            buttonx -= len+1;
            win = retval->buttons[i] = newwin(1, len, buttony, buttonx);
            keypad(win, TRUE);
            nodelay(win, ndelay);
        } // for
    } // if

    texth = h-2;
    if (bcount > 0)
        texth -= 2;
    win = retval->textwin = newwin(texth, w-4, y+1, x+2);
	keypad(win, TRUE);
    nodelay(win, ndelay);
    wbkgdset(win, COLOR_PAIR(MOJOCOLOR_TEXT));
    drawText(retval);

    drawBackground(stdscr);
    wnoutrefresh(stdscr);
    wnoutrefresh(retval->mainwin);
    wnoutrefresh(retval->textwin);
    for (i = 0; i < bcount; i++)
    {
        drawButton(retval, i);
        wnoutrefresh(retval->buttons[i]);
    } // for

    doupdate();  // push it all to the screen.

    return retval;
} // makeBox


static void freeBox(MojoBox *mojobox, boolean clearscreen)
{
    if (mojobox != NULL)
    {
        int i;
        const int bcount = mojobox->buttoncount;
        const int tcount = mojobox->textlinecount;

        if (mojobox->cursval != ERR)
            curs_set(mojobox->cursval);

        for (i = 0; i < bcount; i++)
        {
            free(mojobox->buttontext[i]);
            delwin(mojobox->buttons[i]);
        } // for

        free(mojobox->buttontext);
        free(mojobox->buttons);

        delwin(mojobox->textwin);
        delwin(mojobox->mainwin);

        free(mojobox->title);
        free(mojobox->text);

        for (i = 0; i < tcount; i++)
            free(mojobox->textlines[i]);

        free(mojobox->textlines);
        free(mojobox);

        if (clearscreen)
        {
            wclear(stdscr);
            wrefresh(stdscr);
        } // if
    } // if
} // freeBox


static int upkeepBox(MojoBox **_mojobox, int ch)
{
    static boolean justResized = false;
    MEVENT mevt;
    int i;
    int w, h;
    MojoBox *mojobox = *_mojobox;
    if (mojobox == NULL)
        return -2;

    if (justResized)   // !!! FIXME: this is a kludge.
    {
        justResized = false;
        if (ch == ERR)
            return -1;
    } // if

    switch (ch)
    {
        case ERR:
            return -2;

        case '\r':
        case '\n':
        case KEY_ENTER:
        case ' ':
            return (mojobox->buttoncount <= 0) ? -1 : mojobox->hoverover;

        case '\e':
            return mojobox->buttoncount-1;

        case KEY_UP:
            if (mojobox->textpos > 0)
            {
                mojobox->textpos--;
                drawText(mojobox);
                wrefresh(mojobox->textwin);
            } // if
            return -1;

        case KEY_DOWN:
            getmaxyx(mojobox->textwin, h, w);
            if (mojobox->textpos < (mojobox->textlinecount-h))
            {
                mojobox->textpos++;
                drawText(mojobox);
                wrefresh(mojobox->textwin);
            } // if
            return -1;

        case KEY_PPAGE:
            if (mojobox->textpos > 0)
            {
                getmaxyx(mojobox->textwin, h, w);
                mojobox->textpos -= h;
                if (mojobox->textpos < 0)
                    mojobox->textpos = 0;
                drawText(mojobox);
                wrefresh(mojobox->textwin);
            } // if
            return -1;

        case KEY_NPAGE:
            getmaxyx(mojobox->textwin, h, w);
            if (mojobox->textpos < (mojobox->textlinecount-h))
            {
                mojobox->textpos += h;
                if (mojobox->textpos > (mojobox->textlinecount-h))
                    mojobox->textpos = (mojobox->textlinecount-h);
                drawText(mojobox);
                wrefresh(mojobox->textwin);
            } // if
            return -1;

        case KEY_LEFT:
            if (mojobox->buttoncount > 1)
            {
                if (mojobox->hoverover < (mojobox->buttoncount-1))
                {
                    mojobox->hoverover++;
                    drawButton(mojobox, mojobox->hoverover-1);
                    drawButton(mojobox, mojobox->hoverover);
                    wrefresh(mojobox->buttons[mojobox->hoverover-1]);
                    wrefresh(mojobox->buttons[mojobox->hoverover]);
                } // if
            } // if
            return -1;

        case KEY_RIGHT:
            if (mojobox->buttoncount > 1)
            {
                if (mojobox->hoverover > 0)
                {
                    mojobox->hoverover--;
                    drawButton(mojobox, mojobox->hoverover+1);
                    drawButton(mojobox, mojobox->hoverover);
                    wrefresh(mojobox->buttons[mojobox->hoverover+1]);
                    wrefresh(mojobox->buttons[mojobox->hoverover]);
                } // if
            } // if
            return -1;

        case 12:  // ctrl-L...redraw everything on the screen.
            redrawwin(stdscr);
            wnoutrefresh(stdscr);
            redrawwin(mojobox->mainwin);
            wnoutrefresh(mojobox->mainwin);
            redrawwin(mojobox->textwin);
            wnoutrefresh(mojobox->textwin);
            for (i = 0; i < mojobox->buttoncount; i++)
            {
                redrawwin(mojobox->buttons[i]);
                wnoutrefresh(mojobox->buttons[i]);
            } // for
            doupdate();  // push it all to the screen.
            return -1;

        case KEY_RESIZE:
            mojobox = makeBox(mojobox->title, mojobox->text,
                              mojobox->buttontext, mojobox->buttoncount,
                              mojobox->ndelay, mojobox->hidecursor);
            mojobox->cursval = (*_mojobox)->cursval;  // keep this sane.
            mojobox->hoverover = (*_mojobox)->hoverover;
            freeBox(*_mojobox, false);
            if (mojobox->hidecursor)
                curs_set(0); // make sure this stays sane.
            *_mojobox = mojobox;
            justResized = true;  // !!! FIXME: kludge.
            return -1;

        case KEY_MOUSE:
            if ((getmouse(&mevt) == OK) && (mevt.bstate & BUTTON1_CLICKED))
            {
                int i;
                for (i = 0; i < mojobox->buttoncount; i++)
                {
                    int x1, y1, x2, y2;
                    getbegyx(mojobox->buttons[i], y1, x1);
                    getmaxyx(mojobox->buttons[i], y2, x2);
                    x2 += x1;
                    y2 += y1;
                    if ( (mevt.x >= x1) && (mevt.x < x2) &&
                         (mevt.y >= y1) && (mevt.y < y2) )
                        return i;
                } // for
            } // if
            return -1;
    } // switch

    return -1;
} // upkeepBox


static uint8 MojoGui_ncurses_priority(boolean istty)
{
    if (!istty)
        return MOJOGUI_PRIORITY_NEVER_TRY;  // need a terminal for this!
    else if (getenv("DISPLAY") != NULL)
        return MOJOGUI_PRIORITY_TRY_LAST;  // let graphical stuff go first.
    return MOJOGUI_PRIORITY_TRY_NORMAL;
} // MojoGui_ncurses_priority


static boolean MojoGui_ncurses_init(void)
{
    setlocale(LC_CTYPE, ""); // !!! FIXME: we assume you have a UTF-8 terminal.
    if (initscr() == NULL)
    {
        logInfo("ncurses: initscr() failed, use another UI.");
        return false;
    } // if

	cbreak();
	keypad(stdscr, TRUE);
	noecho();
    start_color();
    mousemask(BUTTON1_CLICKED, NULL);
    init_pair(MOJOCOLOR_BACKGROUND, COLOR_CYAN, COLOR_BLUE);
    init_pair(MOJOCOLOR_BORDERTOP, COLOR_WHITE, COLOR_WHITE);
    init_pair(MOJOCOLOR_BORDERBOTTOM, COLOR_BLACK, COLOR_WHITE);
    init_pair(MOJOCOLOR_BORDERTITLE, COLOR_YELLOW, COLOR_WHITE);
    init_pair(MOJOCOLOR_TEXT, COLOR_BLACK, COLOR_WHITE);
    init_pair(MOJOCOLOR_TEXTENTRY, COLOR_WHITE, COLOR_BLUE);
    init_pair(MOJOCOLOR_BUTTONHOVER, COLOR_YELLOW, COLOR_BLUE);
    init_pair(MOJOCOLOR_BUTTONNORMAL, COLOR_BLACK, COLOR_WHITE);
    init_pair(MOJOCOLOR_BUTTONBORDER, COLOR_WHITE, COLOR_BLUE);
    init_pair(MOJOCOLOR_DONE, COLOR_YELLOW, COLOR_RED);
    init_pair(MOJOCOLOR_TODO, COLOR_CYAN, COLOR_BLUE);

    wbkgdset(stdscr, COLOR_PAIR(MOJOCOLOR_BACKGROUND));
    wclear(stdscr);
    wrefresh(stdscr);

    percentTicks = 0;
    return true;   // always succeeds.
} // MojoGui_ncurses_init


static void MojoGui_ncurses_deinit(void)
{
    freeBox(progressBox, false);
    progressBox = NULL;
    endwin();
    delwin(stdscr);  // not sure if this is safe, but valgrind said it leaks.
    stdscr = NULL;
    free(title);
    title = NULL;
    free(lastComponent);
    lastComponent = NULL;
    free(lastProgressType);
    lastProgressType = NULL;
} // MojoGui_ncurses_deinit


static void MojoGui_ncurses_msgbox(const char *title, const char *text)
{
    char *localized_ok = xstrdup(_("OK"));
    MojoBox *mojobox = makeBox(title, text, &localized_ok, 1, false, true);
    while (upkeepBox(&mojobox, wgetch(mojobox->mainwin)) == -1) {}
    freeBox(mojobox, true);
    free(localized_ok);
} // MojoGui_ncurses_msgbox


static boolean MojoGui_ncurses_promptyn(const char *title, const char *text,
                                        boolean defval)
{
    char *localized_yes = xstrdup(_("Yes"));
    char *localized_no = xstrdup(_("No"));
    char *buttons[] = { localized_yes, localized_no };
    MojoBox *mojobox = makeBox(title, text, buttons, 2, false, true);
    int rc = 0;

    // set the default to "no" instead of "yes"?
    if (defval == false)
    {
        mojobox->hoverover = 1;
        drawButton(mojobox, 0);
        drawButton(mojobox, 1);
        wrefresh(mojobox->buttons[0]);
        wrefresh(mojobox->buttons[1]);
    } // if

    while ((rc = upkeepBox(&mojobox, wgetch(mojobox->mainwin))) == -1) {}
    freeBox(mojobox, true);
    free(localized_yes);
    free(localized_no);
    return (rc == 0);
} // MojoGui_ncurses_promptyn


static MojoGuiYNAN MojoGui_ncurses_promptynan(const char *title,
                                              const char *text,
                                              boolean defval)
{
    char *loc_yes = xstrdup(_("Yes"));
    char *loc_no = xstrdup(_("No"));
    char *loc_always = xstrdup(_("Always"));
    char *loc_never = xstrdup(_("Never"));
    char *buttons[] = { loc_yes, loc_always, loc_never, loc_no };
    MojoBox *mojobox = makeBox(title, text, buttons, 4, false, true);
    int rc = 0;

    // set the default to "no" instead of "yes"?
    if (defval == false)
    {
        mojobox->hoverover = 3;
        drawButton(mojobox, 0);
        drawButton(mojobox, 3);
        wrefresh(mojobox->buttons[0]);
        wrefresh(mojobox->buttons[3]);
    } // if

    while ((rc = upkeepBox(&mojobox, wgetch(mojobox->mainwin))) == -1) {}
    freeBox(mojobox, true);
    free(loc_yes);
    free(loc_no);
    free(loc_always);
    free(loc_never);

    switch (rc)
    {
        case 0: return MOJOGUI_YES;
        case 1: return MOJOGUI_ALWAYS;
        case 2: return MOJOGUI_NEVER;
        case 3: return MOJOGUI_NO;
    } // switch

    assert(false && "BUG: unhandled case in switch statement!");
    return MOJOGUI_NO;
} // MojoGui_ncurses_promptynan


static boolean MojoGui_ncurses_start(const char *_title,
                                     const MojoGuiSplash *splash)
{
    free(title);
    title = xstrdup(_title);
    drawBackground(stdscr);
    wrefresh(stdscr);
    return true;
} // MojoGui_ncurses_start


static void MojoGui_ncurses_stop(void)
{
    free(title);
    title = NULL;
    drawBackground(stdscr);
    wrefresh(stdscr);
} // MojoGui_ncurses_stop


static int MojoGui_ncurses_readme(const char *name, const uint8 *data,
                                    size_t datalen, boolean can_back,
                                    boolean can_fwd)
{
    MojoBox *mojobox = NULL;
    char *buttons[3] = { NULL, NULL, NULL };
    int bcount = 0;
    int backbutton = -99;
    int fwdbutton = -99;
    int rc = 0;
    int i = 0;

    if (can_fwd)
    {
        fwdbutton = bcount++;
        buttons[fwdbutton] = xstrdup(_("Next"));
    } // if

    if (can_back)
    {
        backbutton = bcount++;
        buttons[backbutton] = xstrdup(_("Back"));
    } // if

    buttons[bcount++] = xstrdup(_("Cancel"));

    mojobox = makeBox(name, (char *) data, buttons, bcount, false, true);
    while ((rc = upkeepBox(&mojobox, wgetch(mojobox->mainwin))) == -1) {}
    freeBox(mojobox, true);

    for (i = 0; i < bcount; i++)
        free(buttons[i]);

    if (rc == backbutton)
        return -1;
    else if (rc == fwdbutton)
        return 1;

    return 0;  // error? Cancel?
} // MojoGui_ncurses_readme


static int toggle_option(MojoGuiSetupOptions *parent,
                         MojoGuiSetupOptions *opts, int *line, int target)
{
    int rc = -1;
    if ((opts != NULL) && (target > *line))
    {
        const char *desc = opts->description;
        boolean blanked = false;
        blanked = ( (opts->is_group_parent) && ((!desc) || (!(*desc))) );

        if ((!blanked) && (++(*line) == target))
        {
            const boolean toggled = ((opts->value) ? false : true);

            if (opts->is_group_parent)
                return 0;

            // "radio buttons" in a group?
            if ((parent) && (parent->is_group_parent))
            {
                if (toggled)  // drop unless we weren't the current toggle.
                {
                    // set all siblings to false...
                    MojoGuiSetupOptions *i = parent->child;
                    while (i != NULL)
                    {
                        i->value = false;
                        i = i->next_sibling;
                    } // while
                    opts->value = true;  // reset us to be true.
                } // if
            } // if

            else  // individual "check box" was chosen.
            {
                opts->value = toggled;
            } // else

            return 1;  // we found it, bail.
        } // if

        if (opts->value) // if option is toggled on, descend to children.
            rc = toggle_option(opts, opts->child, line, target);
        if (rc == -1)
            rc = toggle_option(parent, opts->next_sibling, line, target);
    } // if

    return rc;
} // toggle_option


// This code is pretty scary.
static void build_options(MojoGuiSetupOptions *opts, int *line, int level,
                          int maxw, char **lines)
{
    if (opts != NULL)
    {
        const char *desc = opts->description;
        char *spacebuf = (char *) xmalloc(maxw + 1);
        char *buf = (char *) xmalloc(maxw + 1);
        int len = 0;
        int spacing = level * 2;

        if ((desc != NULL) && (*desc == '\0'))
            desc = NULL;

        if (spacing > (maxw-5))
            spacing = 0;  // oh well.

        if (spacing > 0)
            memset(spacebuf, ' ', spacing);  // null-term'd by xmalloc().

        if (opts->is_group_parent)
        {
            if (desc != NULL)
                len = snprintf(buf, maxw-2, "%s%s", spacebuf, desc);
        } // if
        else
        {
            (*line)++;
            len = snprintf(buf, maxw-2, "%s[%c] %s", spacebuf,
                            opts->value ? 'X' : ' ', desc);
        } // else

        free(spacebuf);

        if (len >= maxw-1)
            strcpy(buf+(maxw-4), "...");  // !!! FIXME: Unicode issues!

        if (len > 0)
        {
            const size_t newlen = strlen(*lines) + strlen(buf) + 2;
            *lines = (char*) xrealloc(*lines, newlen);
            strcat(*lines, buf);
            strcat(*lines, "\n");  // I'm sorry, Joel Spolsky!
        } // if

        if ((opts->value) || (opts->is_group_parent))
        {
            int newlev = level + 1;
            if ((opts->is_group_parent) && (desc == NULL))
                newlev--;
            build_options(opts->child, line, newlev, maxw, lines);
        } // if

        build_options(opts->next_sibling, line, level, maxw, lines);
    } // if
} // build_options


static int optionBox(const char *title, MojoGuiSetupOptions *opts,
                     boolean can_back, boolean can_fwd)
{
    MojoBox *mojobox = NULL;
    char *buttons[4] = { NULL, NULL, NULL, NULL };
    boolean ignoreerr = false;
    int lasthoverover = 0;
    int lasttextpos = 0;
    int bcount = 0;
    int backbutton = -99;
    int fwdbutton = -99;
    int togglebutton = -99;
    int cancelbutton = -99;
    int selected = 0;
    int ch = 0;
    int rc = -1;
    int i = 0;

    if (can_fwd)
    {
        fwdbutton = bcount++;
        buttons[fwdbutton] = xstrdup(_("Next"));
    } // if

    if (can_back)
    {
        backbutton = bcount++;
        buttons[backbutton] = xstrdup(_("Back"));
    } // if

    lasthoverover = togglebutton = bcount++;
    buttons[togglebutton] = xstrdup(_("Toggle"));
    cancelbutton = bcount++;
    buttons[cancelbutton] = xstrdup(_("Cancel"));

    do
    {
        if (mojobox == NULL)
        {
            int y = 0;
            int line = 0;
            int maxw, maxh;
            getmaxyx(stdscr, maxh, maxw);
            char *text = xstrdup("");
            build_options(opts, &line, 0, maxw-6, &text);
            mojobox = makeBox(title, text, buttons, bcount, false, true);
            free(text);

            getmaxyx(mojobox->textwin, maxh, maxw);

            if (lasthoverover != mojobox->hoverover)
            {
                const int orighover = mojobox->hoverover;
                mojobox->hoverover = lasthoverover;
                drawButton(mojobox, orighover);
                drawButton(mojobox, lasthoverover);
                wrefresh(mojobox->buttons[orighover]);
                wrefresh(mojobox->buttons[lasthoverover]);
            } // if

            if (lasttextpos != mojobox->textpos)
            {
                mojobox->textpos = lasttextpos;
                drawText(mojobox);
            } // if

            if (selected >= (mojobox->textlinecount - 1))
                selected = mojobox->textlinecount - 1;
            if (selected >= mojobox->textpos+maxh)
                selected = (mojobox->textpos+maxh) - 1;
            y = selected - lasttextpos;

            wattron(mojobox->textwin, COLOR_PAIR(MOJOCOLOR_BUTTONHOVER) | A_BOLD);
            mvwhline(mojobox->textwin, y, 0, ' ', maxw);
            mvwaddstr(mojobox->textwin, y, 0, mojobox->textlines[selected]);
            wattroff(mojobox->textwin, COLOR_PAIR(MOJOCOLOR_BUTTONHOVER) | A_BOLD);
            wrefresh(mojobox->textwin);
        } // if

        lasttextpos = mojobox->textpos;
        lasthoverover = mojobox->hoverover;

        ch = wgetch(mojobox->mainwin);

        if (ignoreerr)  // kludge.
        {
            ignoreerr = false;
            if (ch == ERR)
                continue;
        } // if

        if (ch == KEY_RESIZE)
        {
            freeBox(mojobox, false);  // catch and rebuild without upkeepBox,
            mojobox = NULL;           //  so we can rebuild the text ourself.
            ignoreerr = true;  // kludge.
        } // if

        else if (ch == KEY_UP)
        {
            if (selected > 0)
            {
                WINDOW *win = mojobox->textwin;
                int maxw, maxh;
                int y = --selected - mojobox->textpos;
                getmaxyx(win, maxh, maxw);
                if (selected < mojobox->textpos)
                {
                    upkeepBox(&mojobox, ch);  // upkeepBox does scrolling
                    y++;
                } // if
                else
                {
                    wattron(win, COLOR_PAIR(MOJOCOLOR_TEXT));
                    mvwhline(win, y+1, 0, ' ', maxw);
                    mvwaddstr(win, y+1, 0, mojobox->textlines[selected+1]);
                    wattroff(win, COLOR_PAIR(MOJOCOLOR_TEXT));
                } // else
                wattron(win, COLOR_PAIR(MOJOCOLOR_BUTTONHOVER) | A_BOLD);
                mvwhline(win, y, 0, ' ', maxw);
                mvwaddstr(win, y, 0, mojobox->textlines[selected]);
                wattroff(win, COLOR_PAIR(MOJOCOLOR_BUTTONHOVER) | A_BOLD);
                wrefresh(win);
            } // if
        } // else if

        else if (ch == KEY_DOWN)
        {
            if (selected < (mojobox->textlinecount-1))
            {
                WINDOW *win = mojobox->textwin;
                int maxw, maxh;
                int y = ++selected - mojobox->textpos;
                getmaxyx(win, maxh, maxw);
                if (selected >= mojobox->textpos+maxh)
                {
                    upkeepBox(&mojobox, ch);  // upkeepBox does scrolling
                    y--;
                } // if
                else
                {
                    wattron(win, COLOR_PAIR(MOJOCOLOR_TEXT));
                    mvwhline(win, y-1, 0, ' ', maxw);
                    mvwaddstr(win, y-1, 0, mojobox->textlines[selected-1]);
                    wattroff(win, COLOR_PAIR(MOJOCOLOR_TEXT));
                } // else
                wattron(win, COLOR_PAIR(MOJOCOLOR_BUTTONHOVER) | A_BOLD);
                mvwhline(win, y, 0, ' ', maxw);
                mvwaddstr(win, y, 0, mojobox->textlines[selected]);
                wattroff(win, COLOR_PAIR(MOJOCOLOR_BUTTONHOVER) | A_BOLD);
                wrefresh(win);
            } // if
        } // else if

        else if ((ch == KEY_NPAGE) || (ch == KEY_NPAGE))
        {
            // !!! FIXME: maybe handle this when I'm not so lazy.
            // !!! FIXME:  For now, this if statement is to block
            // !!! FIXME:  upkeepBox() from scrolling and screwing up state.
        } // else if

        else  // let upkeepBox handle other input (button selection, etc).
        {
            rc = upkeepBox(&mojobox, ch);
            if (rc == togglebutton)
            {
                int line = 0;
                rc = -1;  // reset so we don't stop processing input.
                if (toggle_option(NULL, opts, &line, selected+1) == 1)
                {
                    freeBox(mojobox, false);  // rebuild to reflect new options...
                    mojobox = NULL;
                } // if
            } // if
        } // else
    } while (rc == -1);

    freeBox(mojobox, true);

    for (i = 0; i < bcount; i++)
        free(buttons[i]);

    if (rc == backbutton)
        return -1;
    else if (rc == fwdbutton)
        return 1;

    return 0;  // error? Cancel?
} // optionBox


static int MojoGui_ncurses_options(MojoGuiSetupOptions *opts,
                                   boolean can_back, boolean can_fwd)
{
    char *title = xstrdup(_("Options"));
    int rc = optionBox(title, opts, can_back, can_fwd);
    free(title);
    return rc;
} // MojoGui_ncurses_options


static char *inputBox(const char *prompt, int *command, boolean can_back,
                      const char *defval)
{
    char *text = NULL;
    int w, h;
    int i;
    int ch;
    int rc = -1;
    MojoBox *mojobox = NULL;
    size_t retvalalloc = 64;
    size_t retvallen = 0;
    char *retval = NULL;
    char *buttons[3] = { NULL, NULL, NULL };
    int drawpos = 0;
    int drawlen = 0;
    int bcount = 0;
    int backbutton = -1;
    int cancelbutton = -1;

    if (defval == NULL)
        retval = (char *) xmalloc(retvalalloc);
    else
    {
        const size_t defvallen = strlen(defval);
        if ((defvallen * 2) > retvalalloc)
            retvalalloc = defvallen * 2;
        retval = (char *) xmalloc(retvalalloc);
        retvallen = defvallen;
        strcpy(retval, defval);
    } // else

    buttons[bcount++] = xstrdup(_("OK"));

    if (can_back)
    {
        backbutton = bcount++;
        buttons[backbutton] = xstrdup(_("Back"));
    } // if

    cancelbutton = bcount++;
    buttons[cancelbutton] = xstrdup(_("Cancel"));

    getmaxyx(stdscr, h, w);
    w -= 10;
    text = (char *) xmalloc(w+4);
    text[0] = '\n';
    memset(text+1, ' ', w);
    text[w+1] = '\n';
    text[w+2] = ' ';
    text[w+3] = '\0';
    mojobox = makeBox(prompt, text, buttons, bcount, false, false);
    free(text);
    text = NULL;

    do
    {
        getmaxyx(mojobox->textwin, h, w);
        w -= 2;

        if (drawpos >= retvallen)
            drawpos = 0;
        while ((drawlen = (retvallen - drawpos)) >= w)
            drawpos += 5;

        wattron(mojobox->textwin, COLOR_PAIR(MOJOCOLOR_TEXTENTRY) | A_BOLD);
        mvwhline(mojobox->textwin, 1, 1, ' ', w);  // blank line...
        mvwaddstr(mojobox->textwin, 1, 1, retval + drawpos);
        wattroff(mojobox->textwin, COLOR_PAIR(MOJOCOLOR_TEXTENTRY) | A_BOLD);
        wrefresh(mojobox->textwin);

        ch = wgetch(mojobox->mainwin);
        if ( (ch > 0) && (ch < KEY_MIN) && (isprint(ch)) )  // regular key.
        {
            if (retvalalloc <= retvallen)
            {
                retvalalloc *= 2;
                retval = xrealloc(retval, retvalalloc);
            } // if
            retval[retvallen++] = (char) ch;
            retval[retvallen] = '\0';
        } // if

        else if (ch == KEY_BACKSPACE)
        {
            if (retvallen > 0)
                retval[--retvallen] = '\0';
        } // else if

        else if (ch == KEY_RESIZE)
        {
            wrefresh(stdscr);
            getmaxyx(stdscr, h, w);
            w -= 10;
            text = (char *) xrealloc(mojobox->text, w+4);
            text[0] = '\n';
            memset(text+1, ' ', w);
            text[w+1] = '\n';
            text[w+2] = ' ';
            text[w+3] = '\0';
            mojobox->text = text;
            text = NULL;
            upkeepBox(&mojobox, KEY_RESIZE);  // let real resize happen...
        } // else if

        else
        {
            rc = upkeepBox(&mojobox, ch);
        } // else
    } while (rc == -1);

    freeBox(mojobox, true);

    for (i = 0; i < bcount; i++)
        free(buttons[i]);

    if (rc == backbutton)
        *command = -1;
    else if (rc == cancelbutton)
        *command = 0;
    else
        *command = 1;

    if (*command <= 0)
    {
        free(retval);
        retval = NULL;
    } // if

    return retval;
} // inputBox


static char *MojoGui_ncurses_destination(const char **recommends, int recnum,
                                         int *command, boolean can_back,
                                         boolean can_fwd)
{
    char *retval = NULL;
    while (true)
    {
        const char *localized = NULL;
        char *title = NULL;
        char *choosetxt = NULL;
        int rc = 0;

        if (recnum > 0)  // recommendations available.
        {
            int chosen = -1;
            MojoGuiSetupOptions opts;
            MojoGuiSetupOptions *prev = &opts;
            MojoGuiSetupOptions *next = NULL;
            MojoGuiSetupOptions *opt = NULL;
            memset(&opts, '\0', sizeof (MojoGuiSetupOptions));
            int i;
            for (i = 0; i < recnum; i++)
            {
                opt = (MojoGuiSetupOptions *) xmalloc(sizeof (*opt));
                opt->description = recommends[i];
                opt->size = -1;
                prev->next_sibling = opt;
                prev = opt;
            } // for

            choosetxt = xstrdup(_("(I want to specify a path.)"));
            opt = (MojoGuiSetupOptions *) xmalloc(sizeof (*opt));
            opt->description = choosetxt;
            opt->size = -1;
            prev->next_sibling = opt;
            prev = opt;

            opts.child = opts.next_sibling;  // fix this field.
            opts.next_sibling = NULL;
            opts.value = opts.child->value = true;  // make first default.
            opts.is_group_parent = true;
            opts.size = -1;

            title = xstrdup(_("Destination"));
            rc = optionBox(title, &opts, can_back, can_fwd);
            free(title);

            for (i = 0, next = opts.child; next != NULL; i++)
            {
                if (next->value)
                    chosen = i;
                prev = next;
                next = prev->next_sibling;
                free(prev);
            } // for

            free(choosetxt);

            *command = rc;
            if (rc <= 0)  // back or cancel.
                return NULL;

            else if ((chosen >= 0) && (chosen < recnum))  // a specific entry
                return xstrdup(recommends[chosen]);
        } // if

        // either no recommendations or user wants to enter own path...

        localized = _("Enter path where files will be installed.");
        title = xstrdup(localized);
        retval = inputBox(title, &rc, (can_back) || (recnum > 0), NULL);
        free(title);

        // user cancelled or entered text, or hit back and we aren't falling
        //  back to the option list...return.
        if ( (rc >= 0) || ((rc == -1) && (recnum == 0)) )
        {
            *command = rc;
            return retval;
        } // if

        // falling back to the option list again...loop.
    } // while

    // Shouldn't ever hit this, but just in case...
    *command = 0;
    return NULL;
} // MojoGui_ncurses_destination


static int MojoGui_ncurses_productkey(const char *desc, const char *fmt,
                                      char *buf, const int buflen,
                                      boolean can_back, boolean can_fwd)
{
    // !!! FIXME: need text option for (desc).
    // !!! FIXME: need max text entry of (buflen)
    // !!! FIXME: need to disable "next" button if code is invalid.
    char *prompt = xstrdup(_("Please enter your product key"));
    boolean getout = false;
    int retval = 0;

    while (!getout)
    {
        char *text = inputBox(prompt, &retval, can_back, buf);

        if (retval != 1)
            getout = true;
        else
        {
            snprintf(buf, buflen, "%s", text);
            if (isValidProductKey(fmt, text))
                getout = true;
            else
            {
                // !!! FIXME: just improve inputBox.
                // We can't check the input character-by-character, so reuse
                //  the failed-verification localized string.
                char *failtitle = xstrdup(_("Invalid product key"));
                char *failstr = xstrdup(_("That key appears to be invalid. Please try again."));
                MojoGui_ncurses_msgbox(failtitle, failstr);
                free(failstr);
                free(failtitle);
            } // else
        } // else
        free(text);
    } // while

    free(prompt);

    return retval;
} // MojoGui_ncurses_productkey


static boolean MojoGui_ncurses_insertmedia(const char *medianame)
{
    char *fmt = xstrdup(_("Please insert '%0'"));
    char *text = format(fmt, medianame);
    char *localized_ok = xstrdup(_("OK"));
    char *localized_cancel = xstrdup(_("Cancel"));
    char *buttons[] = { localized_ok, localized_cancel };
    MojoBox *mojobox = NULL;
    int rc = 0;

    mojobox = makeBox(_("Media change"), text, buttons, 2, false, true);
    while ((rc = upkeepBox(&mojobox, wgetch(mojobox->mainwin))) == -1) {}

    freeBox(mojobox, true);
    free(localized_cancel);
    free(localized_ok);
    free(text);
    free(fmt);
    return (rc == 0);
} // MojoGui_ncurses_insertmedia


static void MojoGui_ncurses_progressitem(void)
{
    // no-op in this UI target.
} // MojoGui_ncurses_progressitem


static boolean MojoGui_ncurses_progress(const char *type, const char *component,
                                        int percent, const char *item,
                                        boolean can_cancel)
{
    const uint32 now = ticks();
    boolean rebuild = (progressBox == NULL);
    int ch = 0;
    int rc = -1;

    if ( (lastComponent == NULL) ||
         (strcmp(lastComponent, component) != 0) ||
         (lastProgressType == NULL) ||
         (strcmp(lastProgressType, type) != 0) ||
         (lastCanCancel != can_cancel) )
    {
        free(lastProgressType);
        free(lastComponent);
        lastProgressType = xstrdup(type);
        lastComponent = xstrdup(component);
        lastCanCancel = can_cancel;
        rebuild = true;
    } // if

    if (rebuild)
    {
        int w, h;
        char *text = NULL;
        char *localized_cancel = (can_cancel) ? xstrdup(_("Cancel")) : NULL;
        char *buttons[] = { localized_cancel };
        const int buttoncount = (can_cancel) ? 1 : 0;
        char *spacebuf = NULL;
        getmaxyx(stdscr, h, w);
        w -= 10;
        text = (char *) xmalloc((w * 3) + 16);
        if (snprintf(text, w, "%s", component) > (w-4))
            strcpy((text+w)-4, "...");  // !!! FIXME: Unicode problem.
        strcat(text, "\n\n");
        spacebuf = (char *) xmalloc(w+1);
        memset(spacebuf, ' ', w);  // xmalloc provides null termination.
        strcat(text, spacebuf);
        free(spacebuf);
        strcat(text, "\n\n ");

        freeBox(progressBox, false);
        progressBox = makeBox(type, text, buttons, buttoncount, true, true);
        free(text);
        free(localized_cancel);
    } // if

    // limit update spam... will only write every one second, tops.
    if ((rebuild) || (percentTicks <= now))
    {
        static boolean unknownToggle = false;
        char *buf = NULL;
        WINDOW *win = progressBox->textwin;
        int w, h;
        getmaxyx(win, h, w);
        w -= 2;
        buf = (char *) xmalloc(w+1);

        if (percent < 0)
        {
            const boolean origToggle = unknownToggle;
            int i;
            wmove(win, h-3, 1);
            for (i = 0; i < w; i++)
            {
                if (unknownToggle)
                    waddch(win, ' ' | COLOR_PAIR(MOJOCOLOR_TODO));
                else
                    waddch(win, ' ' | COLOR_PAIR(MOJOCOLOR_DONE));
                unknownToggle = !unknownToggle;
            } // for
            unknownToggle = !origToggle;  // animate by reversing next time.
        } // if
        else
        {
            int cells = (int) ( ((double) w) * (((double) percent) / 100.0) );
            snprintf(buf, w+1, "%d%%", percent);
            mvwaddstr(win, h-3, ((w+2) - utf8len(buf)) / 2, buf);
            mvwchgat(win, h-3, 1, cells, A_BOLD, MOJOCOLOR_DONE, NULL);
            mvwchgat(win, h-3, 1+cells, w-cells, A_BOLD, MOJOCOLOR_TODO, NULL);
        } // else

        wtouchln(win, h-3, 1, 1);  // force reattributed cells to update.

        if (snprintf(buf, w+1, "%s", item) > (w-4))
            strcpy((buf+w)-4, "...");  // !!! FIXME: Unicode problem.
        mvwhline(win, h-2, 1, ' ', w);
        mvwaddstr(win, h-2, ((w+2) - utf8len(buf)) / 2, buf);

        free(buf);
        wrefresh(win);

        percentTicks = now + 1000;
    } // if

    // !!! FIXME: check for input here for cancel button, resize, etc...
    ch = wgetch(progressBox->mainwin);
    if (ch == KEY_RESIZE)
    {
        freeBox(progressBox, false);
        progressBox = NULL;
    } // if
    else if (ch == ERR)  // can't distinguish between an error and a timeout!
    {
        // do nothing...
    } // else if
    else
    {
        rc = upkeepBox(&progressBox, ch);
    } // else

    return (rc == -1);
} // MojoGui_ncurses_progress


static void MojoGui_ncurses_final(const char *msg)
{
    char *title = xstrdup(_("Finish"));
    MojoGui_ncurses_msgbox(title, msg);
    free(title);
} // MojoGui_ncurses_final

// end of gui_ncurses.c ...

